﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Red.Core
{
    /// <summary>
    ///     Base class for attributes that can be subject of ClassMap query
    /// </summary>
    public class MappableAttribute : Attribute
    {
    }

    /// <summary>
    ///     Maps classes basing on different criteria. As iterating through classes is slow, it's intended to do the iteration
    ///     and classification once, and then only expose cached data.
    /// </summary>
    public static class ClassMap
    {
        #region Data
        
        private static readonly Dictionary< Type, List<Type> > _byAttributeClass = new Dictionary< Type, List<Type> >();
        private static List< Action< Dictionary< Type, List<Type> >>> _updateCallbacks = new List<Action<Dictionary< Type, List<Type> >>>();

        #endregion Data

        #region C-tor

        static ClassMap()
        {
            // Hacky: force load controls libraries.
            // is there a better way to load controls libraries that are supposed to be used only through the interop extension?
            foreach ( string dll in Directory.GetFiles( Environment.CurrentDirectory, "Red.Controls*.dll", SearchOption.AllDirectories ) )
            {
                try
                {
                    Assembly.LoadFrom( dll );
                }
                catch ( Exception ex ) when ( ex is FileLoadException || ex is BadImageFormatException )
                {
                    // assembly already loaded or file is not an assembly
                }
            }

            foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
                HandleAssembly( assembly );

            AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoaded;
        }

        #endregion C-tor

        #region Public methods

        /// <summary>
        ///     Visits all the classes with specified attribute, additional classes will be visited every time an additional assembly is loaded
        /// </summary>
        /// <typeparam name="AttrT">
        ///     Type of attribute you want to list (has to be final type, no base classes supported)
        /// </typeparam>
        /// <param name="visitor">
        ///     Gets called for every class that meets the criteria
        /// </param>
        public static void RegisterClassesWithAtrributeListener<AttrT>( Action<Type, AttrT> visitor ) where AttrT : MappableAttribute
        {
            VisitClassesWithAttributeInternal<AttrT>( visitor, _byAttributeClass );
            _updateCallbacks.Add( ( Dictionary< Type, List<Type> > newAttributes ) => ClassMap.VisitClassesWithAttributeInternal<AttrT>( visitor, newAttributes ) );
        }

        private static void VisitClassesWithAttributeInternal<AttrT>( Action<Type, AttrT> visitor, Dictionary< Type, List<Type> > byAttributeClass )
        {
            Type attributeType = typeof( AttrT );
            List<Type> types;
            if ( byAttributeClass.TryGetValue( attributeType, out types ) )
            {
                foreach ( Type type in types )
                {
                    // We can safely access element [0] as we know upfront it contains the attribute of this type. On the other
                    // hand we assume there is only one attribute of each kind to simplify the API - it can be extended if needed
                    AttrT attr = (AttrT)type.GetCustomAttributes( attributeType, false )[0];
                    visitor( type, attr );
                }
            }
        }

        #endregion Public methods

        #region Private methods

        private static void OnAssemblyLoaded( object sender, AssemblyLoadEventArgs e )
        {
            var assembly = e.LoadedAssembly;
            HandleAssembly( assembly );
        }

        private static void HandleAssembly( Assembly assembly )
        {
            Dictionary< Type, List< Type >> newAttributes = new Dictionary<Type, List<Type>>();
            foreach ( Type type in assembly.GetTypes() )
            {
                var attribs = type.GetCustomAttributes( typeof( MappableAttribute ), false );
                foreach ( MappableAttribute attr in attribs )
                {
                    Type attrType = attr.GetType();
                    List<Type> types;
                    if ( !newAttributes.TryGetValue( attrType, out types ) )
                    {
                        types = new List<Type>();
                        newAttributes.Add( attrType, types );
                    }
                    types.Add( type );
                }
            }

            foreach ( var callback in _updateCallbacks )
                callback( newAttributes );

            foreach ( var item in newAttributes )
            {
                List<Type> types;
                if ( !_byAttributeClass.TryGetValue( item.Key, out types ) )
                    _byAttributeClass.Add( item.Key, item.Value );
                else
                    types.AddRange( item.Value );
            }
        }

        #endregion Private methods
    }
}
